پرش به مطلب اصلی

Designing data intensive application

نام کتاب: Designing data intensive application

سال چاپ:۱۳۹۷

سال خوندن من: در حال خوندن


فصل ۱: Reliable، Scalable و Maintainable Applications

این فصل اول کتاب Designing Data-Intensive Applications نوشته‌ی Martin Kleppmann هست و پایه‌ای‌ترین مفاهیم مربوط به سیستم‌های داده‌ای رو توضیح می‌ده. این فصل در واقع هم اصطلاحات رو روشن می‌کنه و هم مسیر بقیه‌ی کتاب رو مشخص می‌کنه، با این تمرکز که Reliability، Scalability و Maintainability دقیقاً یعنی چی و چطور می‌شه بهشون رسید.

امروزه خیلی از اپلیکیشن‌ها data-intensive هستن. یعنی محدودیت اصلیشون CPU نیست؛ مشکل معمولاً از حجم زیاد دیتا، پیچیدگی اون یا سرعت تغییراتش میاد. برای اینکه بتونیم اپلیکیشن‌های data-intensive موفق بسازیم، کتاب روی سه موضوع کلیدی تمرکز می‌کنه:

Reliability

سیستمی که reliable باشه حتی وقتی مشکل پیش میاد هم درست کار می‌کنه، وظیفه‌هاش رو انجام می‌ده و سطح performance قابل‌قبولش رو حفظ می‌کنه. این یعنی بتونه خطاهای مختلف رو تحمل کنه، مثل:

  • Hardware faults: مثل سوختن هارد، خرابی RAM، قطع برق یا حتی کابل برق که یکی اشتباهی می‌کشه بیرون! اینا تو دیتاسنترهای بزرگ خیلی رایجه.

  • Software errors (bugs): معمولاً سیستمی هستن و نسبت به خطاهای سخت‌افزاری دردسر بیشتری دارن.

  • Human errors: خطاهای انسانی همیشه بخشی از کار هستن. سیستم‌های robust طوری طراحی می‌شن که احتمال خطای انسانی کم بشه و اگر هم اتفاق افتاد سریع بشه ریکاوری کرد.

برای افزایش Reliability معمولاً از تکنیک‌هایی مثل fault-tolerance، مانیتورینگ دقیق (telemetry) و همین‌طور مدیریت درست و آموزش تیم‌ها استفاده می‌شه.


Scalability

سیستمی که scalable باشه، وقتی دیتا یا ترافیک یا پیچیدگی بیشتر می‌شه، هنوز می‌تونه درست مدیریت کنه. برای اینکه در مورد scalability درست صحبت کنیم، باید load سیستم رو کمی‌سازی کنیم، مثلاً با پارامترهایی مثل تعداد request بر ثانیه، نسبت read به write یا تعداد کاربرهای active.

مثال: تحویل home timeline در Twitter.

  • یه روش اینه که هر بار کاربر timeline رو باز می‌کنه، توییت‌های فالووینگ‌هاش fetch بشن. این برای کسایی که خیلی فالووینگ دارن می‌تونه سنگین بشه (read load بالا).
  • روش دیگه اینه که وقتی کاربر توییت می‌کنه، اون توییت رو pre-compute کنیم و برای همه‌ی فالوورهاش push کنیم (fan-out) تا خوندن سریع بشه. این بار روی write می‌ذاره، مخصوصاً برای کاربرهای معروف.
  • Twitter در عمل یه hybrid strategy استفاده می‌کنه: برای بیشتر کاربرا fan-out و برای celebrityها fetch موقع read.

برای سنجش performance هم بهتره از percentileهای زمان پاسخ (مثلاً p95 یا p99) استفاده کنیم، نه فقط average. چون "tail latency" همون تجربه‌ایه که خیلی از کاربرا حس می‌کنن و می‌تونه رضایتشون رو به شدت تحت‌تأثیر بذاره.


Maintainability

این موضوع مربوط به اینه که آدمای مختلف (از تیم engineering گرفته تا ops) بتونن در طول زمان به راحتی روی سیستم کار کنن، فیچرهای موجود رو نگه دارن و سیستم رو برای نیازهای جدید تغییر بدن. سه بخش مهم Maintainability:

  • Operability: راحت بودن مدیریت سیستم برای تیم ops. یعنی مانیتورینگ خوب، مستندات شفاف، آپگرید راحت و پشتیبانی درست.

  • Simplicity: کنترل complexity. پیچیدگی می‌تونه از state زیاد، coupling شدید یا dependencyهای درهم‌تنیده بیاد. کم کردن complexity خیلی مهمه برای کاهش ریسک باگ و آسون‌تر کردن نگهداری. اینجا داشتن abstractionهای درست کمک می‌کنه سیستم رو به تیکه‌های کوچیک و reusable تقسیم کنیم.

  • Evolvability: قابلیت تغییر راحت سیستم و تطبیق با نیازهای جدید. این بخش خیلی به simplicity و abstractionهای درست وابسته‌ست.

Fault vs Failure

به طور کلی یه Fault یعنی یکی از کامپوننت‌های سیستم از چیزی که توی specification براش تعریف شده منحرف بشه. ولی این با Failure فرق داره: Failure وقتی اتفاق می‌افته که کل سیستم دیگه نتونه سرویس موردنیاز رو به کاربر بده.

Reliability توی سیستم‌های نرم‌افزاری یعنی حتی وقتی این «چیزهایی که می‌تونن خراب بشن» (Faultها) پیش میان، سیستم همچنان درست کار کنه. چون حذف کامل Faultها عملاً غیرممکنه، سیستم‌ها طوری طراحی می‌شن که fault-tolerance mechanisms داشته باشن تا نذارن Faultها به Failure تبدیل بشن.

یه راه تست این مکانیزم‌ها اینه که عمداً Fault تزریق کنیم. مثلاً ابزار معروف Netflix به اسم Chaos Monkey همین کارو می‌کنه: میاد بخشی از سیستم رو می‌ترکونه تا مطمئن بشیم سیستم می‌تونه زنده بمونه.


دسته‌های مختلف Fault

۱. Hardware Faults

این‌ها مشکلات فیزیکی هستن، مثل:

  • سوختن هارد
  • خرابی RAM
  • قطع برق دیتاسنتر
  • یا حتی یه نفر کابل شبکه رو بکشه بیرون

تو دیتاسنترهای بزرگ این اتفاقات زیاده، ولی معمولاً random و independent در نظر گرفته می‌شن.

۲. Software Errors

این‌ها خطاهای سیستمی هستن که خیلی سخت‌تر می‌شه پیش‌بینیشون کرد. ممکنه روی چندین node با هم اتفاق بیفته و باعث system failure گسترده‌تر بشه.
مثال‌ها:

  • یه bug که اگه ورودی خاصی بهش بدی کل application server رو crash کنه
  • پروسه‌هایی که runaway می‌شن و همه‌ی resource مشترک رو می‌بلعن.
  • سرویس‌های وابسته‌ای که کند یا بی‌پاسخ می‌شن.
  • اcascading failures (یعنی یه مشکل کوچیک باعث دومینویی از خطاها بشه).

۳. Human Errors

مثلا Configuration errorها یکی از اصلی‌ترین دلایل outage تو سرویس‌های بزرگ اینترنتی هستن، حتی بیشتر از hardware faults.


Faultها توی Distributed Systems

وقتی سیستم روی چندین ماشین توی شبکه اجرا می‌شه، داستان فرق می‌کنه. اینجا partial failures خیلی رایجن: بعضی بخش‌ها خراب می‌شن، بقیه درست کار می‌کنن. این موضوع خودش Faultهای جدیدی می‌سازه:

۱. Network Faults

شبکه بین ماشین‌ها همیشه قابل اعتماد نیست. ممکنه:

  • پیام (packet) گم بشه،
  • تأخیر داشته باشه،
  • دوباره ارسال بشه،
  • یا حتی ترتیبش بهم بخوره.

و هیچ تضمینی هم برای زمان و تحویلش نیست. فرستنده هم بدون جواب، نمی‌تونه مطمئن بشه که پیام رسیده. برای همین معمولاً از timeout استفاده می‌کنیم (که کامل هم درست کار نمی‌کنه). Congestion و صف‌زدن (queueing delay) هم دلایل اصلی نوسان performance شبکه‌ان.

۲. Unreliable Clocks

هر ماشین ساعت خودش رو داره که ممکنه drift کنه (یعنی سریع‌تر یا کندتر بشه). حتی با پروتکل‌هایی مثل NTP هم ممکنه ساعت‌ها sync نباشن. گاهی ساعت می‌تونه یه‌دفعه به عقب یا جلو بپره.
اگه برای ordering رویدادها روی چند node به این ساعت‌ها تکیه کنیم، ممکنه باعث data loss یا رفتار اشتباه بشه. مثلاً استراتژی last write wins توی conflict resolution ممکنه به خاطر همین مشکل fail کنه.

۳. Process Pauses

یه پروسه ممکنه یه‌دفعه برای مدت طولانی pause بشه، مثلاً به خاطر:

  • اstop-the-world توی garbage collection،
  • اsynchronous disk I/O،
  • یا page fault.

وقتی این pause اتفاق می‌افته، بقیه‌ی nodeها فکر می‌کنن این node مرده و declareش می‌کنن dead. حالا اگه دوباره بیدار بشه و بخواد با اطلاعات قدیمی کار کنه، مشکل درست می‌شه.

۴. Byzantine Faults

اینجا ماجرا خیلی بدتره: node ممکنه رفتار arbitrary یا حتی malicious داشته باشه. یعنی عمداً پیام‌های متناقض یا خراب بفرسته تا بقیه‌ی nodeها رو گمراه کنه.
البته پروتکل‌های Byzantine fault-tolerant وجود دارن، ولی بیشتر سیستم‌های داده‌ای server-side فرض می‌کنن nodeها ممکنه unreliable باشن، ولی dishonest نیستن (یعنی عمداً دروغ نمی‌گن).

توصیف Load

اLoad یعنی تقاضا یا فشار کاری که روی سیستم میاد. Scalability هم یعنی توانایی سیستم برای هندل کردن همین افزایش Load.

برای اینکه بتونیم از رشد حرف بزنیم، اول باید Load فعلی سیستم رو با چند تا عدد خلاصه کنیم؛ به اینا می‌گن load parameters. اینکه دقیقاً چه پارامترهایی رو انتخاب کنیم، بستگی به معماری سیستم داره.

مثال‌هایی از load parameters:

  • تعداد requests per second توی یه web server
  • نسبت read به write توی یه database
  • تعداد کاربران فعال همزمان توی یه chat room
  • اhit rate یه cache

مثال توییتر خیلی خوب اینو نشون می‌ده. توی نوامبر ۲۰۱۲:

  • به طور میانگین ۴.۶k و در اوج ۱۲k request/second برای tweet کردن داشتن.

  • ولی برای timeline دیدن ۳۰۰k request/second بود.

حالا شاید هندل کردن ۱۲k write/second ساده به نظر بیاد، اما مشکل اصلی توییتر fan-out بود. یعنی هر کاربر هم فالوئر زیاد داره، هم فالوئینگ زیاد. یه توییت از یه سلبریتی می‌تونه به ۳۰ میلیون home timeline نوشته بشه! پس توی این سناریو، توزیع فالوئرهای هر کاربر خودش یه load parameter کلیدی حساب می‌شه.


توصیف Performance

وقتی Load رو شناختیم، بعدش باید بررسی کنیم که Performance سیستم با افزایش Load چه تغییری می‌کنه. معمولاً دو جور نگاه داریم:

  1. اگه یه load parameter زیاد بشه و منابع (CPU، Memory، Network Bandwidth) ثابت بمونن، Performance چه اتفاقی براش میفته؟
  2. اگه بخوایم Performance ثابت بمونه، باید چه‌قدر منابع رو افزایش بدیم وقتی load parameter بالا می‌ره؟

متریک‌های Performance به نوع سیستم بستگی دارن:

  • توی batch processing systems مثل Hadoop، معمولاً Throughput مهم‌ترین معیار محسوب می‌شه (یعنی چند رکورد در ثانیه پردازش می‌شن یا کل job روی یه dataset چه مدت طول می‌کشه).
  • توی online systems، بیشتر Response Time مهمه (یعنی فاصله بین فرستادن request از سمت client تا گرفتن response).

Response Time و Latency

خیلی وقتا "Latency" و "Response Time" رو یکی می‌دونن، ولی دقیقا یکی نیستن:

  • اResponse Time همون چیزیه که کاربر می‌بینه: شامل زمان پردازش request (service time) + network delay + queueing delay.
  • اLatency فقط اون بخشی از زمانه که request منتظر می‌مونه تا سرویس داده بشه.

اResponse time یه عدد ثابت نیست، بلکه یه distribution داره. حتی دو تا request یکسان هم می‌تونن response time متفاوتی داشته باشن (به خاطر context switch، packet retransmission، GC pause، یا disk I/O).

برای اینکه Performance رو درست بفهمیم، استفاده از Percentiles خیلی بهتر از Mean (Average) ـه، چون میانگین نشون نمی‌ده چند درصد کاربرا واقعاً یه delay خاص رو تجربه کردن.

  • اMedian (50th percentile): نصف requestها سریع‌تر از این هستن، نصف دیگه کندتر. این شاخص خوبیه برای "تجربه‌ی معمولی کاربر".

  • اHigh percentiles یا همون Tail Latencies (مثل 95th، 99th یا 99.9th): خیلی مهمن، چون تجربه‌ی کندترین کاربرا رو نشون می‌دن. مثلاً Amazon فهمید فقط ۱۰۰ms افزایش response time می‌تونه فروش رو ۱٪ کم کنه!

اTail latency معمولا پایه‌ی SLO و SLA ـهاست که سطح انتظار کارایی و در دسترس بودن سرویس رو تعریف می‌کنن.


نقش Queueing

اQueueing delay یکی از دلایل اصلی tail latency بالاست. حتی تعداد کمی request کند می‌تونه کل صف رو عقب بندازه و باعث head-of-line blocking بشه. به همین دلیل، خیلی مهمه که response time رو از سمت client بسنجیم.

وقتی یه request end-user شامل چندین backend call باشه، tail latency amplification اتفاق می‌افته: یعنی حتی اگه فقط درصد کمی از backend callها کند باشن، تعداد خیلی بیشتری از درخواست‌های کاربر نهایی کند می‌شن.

برای مانیتورینگ درست Percentiles هم الگوریتم‌های خاصی لازمه (مثل forward decay، t-digest یا HdrHistogram)، چون اینکه بخوای کل داده‌ها رو مرتب کنی خیلی inefficient می‌شه.

وقتی یه سیستم با افزایش load (تقاضا) روبه‌رو میشه، لازمه یه سری استراتژی داشته باشیم که performance (عملکرد) خوب باقی بمونه. هیچ راه‌حل جادویی یا یکسانی برای همه‌چی وجود نداره. معماری درست، خیلی وابسته‌ست به نیاز اپلیکیشن و نوع load‌ای که می‌گیره.

راه‌های اصلی برای مقابله با load:

  1. اScaling Up (Vertical Scaling)
    این یعنی انتقال به یه ماشین قوی‌تر. مثلاً یه سرور قدرتمند با کلی CPU، رم زیاد و دیسک‌های متعدد. اینا مثل یه ماشین واحد کار می‌کنن و یه interconnect سریع دارن که هر CPU می‌تونه به هر بخش از حافظه یا دیسک دسترسی داشته باشه.
    مدیریت این مدل راحت‌تره، ولی هزینه‌ش به‌صورت خطی بالا نمیره؛ یعنی اگه ماشین دو برابر بزرگ‌تر بگیری، لزوماً دو برابر load رو نمی‌کشه. همینطور محدودیت‌هایی مثل bottleneck و تک‌مکان جغرافیایی هم داره. البته بعضی high-end server ها قطعات hot-swappable دارن برای fault tolerance، ولی باز محدودیت دارن.

  2. اScaling Out (Horizontal Scaling)
    این یعنی پخش کردن load روی چندتا ماشین کوچیک‌تر، که بهش میگن shared-nothing architecture. تو این مدل، هر node (با CPU، رم و دیسک خودش) از طریق یه شبکه عادی به بقیه وصله.
    مزیتش اینه که دیتای بزرگ می‌تونه روی چندتا دیسک پخش بشه و query ها هم بین چندتا CPU تقسیم بشن. اگه query فقط روی یه partition اجرا بشه، اون node می‌تونه مستقل کار کنه، و با اضافه کردن node جدید، throughput بالا میره.
    مشکلش اینه که distributed shared-nothing معماری پیچیدگی بیشتری میاره و بعضی وقتا data model ها رو محدود می‌کنه.

    🔹 اPartitioning (Sharding)
    یکی از کلیدی‌ترین روش‌ها برای scaling out همینه. دیتابیس بزرگ رو می‌شکونیم به چندتا partition کوچیک‌تر و می‌دیم به nodeهای مختلف. اینطوری هم دیتا پخش میشه هم query load. هدف اینه که از hot spot جلوگیری بشه (یعنی جایی که یه partition بیش از حد پرکار میشه).

    روش‌های partitioning:

    • اKey Range Partitioning: کلیدها رو sort می‌کنیم و هر partition یه بازه از کلیدها رو می‌گیره. برای range query خیلی خوبه، ولی خطر hot spot داره.

    • اPartitioning by Hash of Key: کلیدها رو hash می‌کنیم تا یکنواخت‌تر پخش بشن. اینطوری احتمال hot spot کمتره. Consistent hashing هم توی سیستم‌های caching اینترنتی معروفه.

    • Partitioning with Secondary Indexes: این یکی سخت‌تره چون secondary index مستقیم روی یه partition نمی‌شینه. دو تا روش معروف داره:

      -ا Document-based (ایندکس کنار document ذخیره میشه → query باید scatter/gather کنه)

      • اTerm-based (ایندکس global میشه → read سریع‌تر، ولی write پیچیده‌تر میشه)

    🔹 اReplication
    معمولاً همراه partitioning استفاده میشه. یعنی چندتا کپی از یه دیتا روی nodeهای مختلف. اینطوری هم fault tolerance داریم هم performance بهتر.

    روش‌های replication:

    • اSingle-Leader Replication: همه writeها میرن یه leader، بعد به followerها replicate میشه. ساده‌تره ولی failover لازمه.

    • اMulti-Leader Replication: چندتا node می‌تونن write بگیرن و async به هم replicate می‌کنن. برای multi-datacenter خوبه ولی conflict resolution سخت میشه.

    • اLeaderless Replication: هر replica می‌تونه write رو مستقیم بگیره. معمولا با quorum (تعداد مشخصی node باید تأیید بدن) consistency رو حفظ می‌کنن. robust هست، ولی معمولاً linearizability نداره.

  3. اElastic Systems vs. Manually Scaled Systems

    • اElastic Systems: خودشون وقتی load میره بالا، منابع اضافه می‌کنن. برای workloadهای غیرقابل‌پیش‌بینی خیلی خوبن.

    • اManually Scaled Systems: باید آدم‌ها دستی تحلیل کنن و تصمیم بگیرن کی ماشین اضافه بشه. ساده‌تره و سورپرایز کمتری داره.

  4. No One-Size-Fits-All Architecture
    بهترین معماری بستگی داره به خصوصیات اپلیکیشن: تعداد readها و writeها، حجم دیتا، پیچیدگی دیتا، نیاز به response time و الگوهای دسترسی.
    مثلاً سیستمی که ۱۰۰هزار درخواست کوچیک در ثانیه رو هندل می‌کنه، زمین تا آسمون فرق داره با سیستمی که فقط ۳ درخواست سنگین در دقیقه می‌گیره، حتی اگه throughput کلی مشابه باشه. برای startupها هم معمولاً مهم‌تره که سریع فیچر بسازن تا اینکه خودشونو درگیر scaling زودهنگام کنن.

  5. General-Purpose Building Blocks
    با اینکه معماری‌ها اختصاصی هستن، ولی معمولاً از یه سری building block عمومی ساخته میشن. شناخت این پایه‌ها خیلی مهمه برای طراحی سیستم scalable.